﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.IO
{
    using System;
    using System.IO;

    /// <summary>Provides a thin wrapper for a buffer that is filled by calling the provided callback.</summary>
    /// <remarks>
    /// <para>The fields in this class and its base are not encapsulated properly for performance reasons. For example,
    /// <see cref="Buffer.this"/> along with <see cref="Index"/> should be replaced with a method like
    /// <c>GetBufferedByte()</c>. However, doing so costs roughly 33% of the throughput in a class like e.g.
    /// <see cref="Base64Stream"/> when run on the emulator. This most likely stems form the fact that the CF JIT is
    /// only able to inline very simple methods. Apparently, even a method with the one-liner <c>return
    /// this.buffer[this.offset++];</c> is not eligible for inlining.</para>
    /// </remarks>
    internal sealed class ReadBuffer : Buffer
    {
        private readonly Func<byte[], int, int, int> readCallback;
        private int index;
        private int lastLineBreakIndex = int.MinValue;

        /// <summary>Initializes a new instance of the <see cref="ReadBuffer"/> class.</summary>
        /// <param name="readCallback">The method that is called when the buffer needs to be filled. This method must
        /// behave exactly like the <see cref="Stream.Read(byte[], int, int)">Stream.Read</see> method.</param>
        /// <param name="bufferSize">The size of the internal buffer in bytes.</param>
        internal ReadBuffer(Func<byte[], int, int, int> readCallback, int bufferSize) : base(bufferSize)
        {
            this.readCallback = readCallback;
        }

        /// <summary>Gets or sets the current index into the internal buffer.</summary>
        internal int Index
        {
            get { return this.index; }
            set { this.index = value; }
        }

        /// <summary>If the range [<see cref="Index"/>, <see cref="Buffer.Count"/>) contains at least one "\r\n"
        /// sequence starting at a position > <see cref="Index"/> then returns an <see cref="ArraySegment{T}"/> object
        /// that references said range. Otherwise, calls <see cref="Fill"/> and returns an <see cref="ArraySegment{T}"/>
        /// object referencing the whole buffer.</summary>
        /// <exception cref="InvalidOperationException">The stream of bytes retrieved through the read callback
        /// specified at construction contains a line that is longer than the specified buffer size.</exception>
        /// <remarks>
        /// <para><b>Caution</b>: Each <see cref="ArraySegment{T}"/> object retrieved by a call to this method is
        /// immediately invalidated when this method is called the next time.</para>
        /// <para>The returned segment contains, starting at <see cref="Index"/>, at least one full line including the
        /// line break at the end, if such a line exists; otherwise the segment contains whatever bytes appear at the end
        /// of the stream.</para>
        /// <para>This method assumes that every '\r' character is always followed by a '\n' character.</para>
        /// </remarks>
        internal ArraySegment<byte> GetLine()
        {
            while ((this.lastLineBreakIndex == int.MinValue) ||
                ((this.lastLineBreakIndex <= this.index) && this.Fill()))
            {
                this.lastLineBreakIndex = this.GetLastLineBreakIndex();

                if ((this.Count == this.Capacity) && (this.lastLineBreakIndex <= 0))
                {
                    throw new InvalidOperationException(
                        "The read callback must not deliver a line longer than the current buffer size of " +
                            this.Capacity + " bytes.");
                }
            }

            return new ArraySegment<byte>(this.GetBuffer(), this.index, this.Count - this.index);
        }

        /// <summary>Fills the internal buffer by calling the callback specified during construction.</summary>
        /// <returns><c>true</c> when at least one byte was read; otherwise <c>false</c>.</returns>
        /// <remarks>
        /// <para>After each call to this method, <see cref="Buffer.Count"/> contains the number of bytes in the buffer
        /// and <see cref="Index"/> equals 0.</para>
        /// <para>This function is usually called when <see cref="Index"/> equals <see cref="Buffer.Count"/>, that is,
        /// when the client has processed all bytes in the buffer (the whole buffer is filled in this case). If it is
        /// called earlier (when <see cref="Index"/> &lt; <see cref="Buffer.Count"/>) then the remaining bytes at the
        /// end of the buffer are first copied to the start of the buffer and the now empty part of the buffer is filled
        /// by calling the callback.</para>
        /// </remarks>
        internal bool Fill()
        {
            int remainingCount = this.Count - this.index;
            System.Buffer.BlockCopy(this.GetBuffer(), this.index, this.GetBuffer(), 0, remainingCount);
            this.index = 0;
            this.lastLineBreakIndex = int.MinValue;
            this.Count = remainingCount;
            this.Count += this.readCallback(this.GetBuffer(), remainingCount, this.Capacity - remainingCount);
            return this.Count > remainingCount;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private int GetLastLineBreakIndex()
        {
            int result;

            // -2 to allow for the '\n' that follows the '\r'
            for (result = this.Count - 2; (result >= this.index) && (this.GetBuffer()[result] != (byte)'\r'); --result)
            {
            }

            return result;
        }
    }
}
